﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls.Primitives;

namespace Kokomo.Controls
{
	public class DiagramPanel : Panel
	{
		static DiagramPanel()
		{
			DefaultStyleKeyProperty.OverrideMetadata(typeof(DiagramPanel), new FrameworkPropertyMetadata(typeof(DiagramPanel)));
		}

		private FrameworkElement GetTargetElement(object item)
		{
			ItemsControl itemsOwner = ItemsControl.GetItemsOwner(this);
			FrameworkElement targetElement = item as FrameworkElement;
			if (
				(targetElement == null)
				&& (itemsOwner != null)
				&& (this.IsItemsHost)
				)
			{
				targetElement = itemsOwner.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
			}

			return targetElement;
		}

		#region Layout
		protected override Size MeasureOverride(Size availableSize)
		{
			Size size = new Size();
			//return base.MeasureOverride(availableSize);

			foreach (UIElement element in this.Children)
			{
				element.ClearValue(DepthProperty);
			}

			Stack<DiagramItem> ancestorStack = new Stack<DiagramItem>();
			foreach (UIElement element in this.Children)
			{
				this.MeasureDepth(element, 0, ancestorStack);
			}

			List<UIElement> measuredElements = new List<UIElement>(this.Children.Count);
			foreach (UIElement element in this.Children)
			{
				if (
					(GetDepth(element) == 0)
					&& !measuredElements.Contains(element)
					)
				{
					Size branchSize = this.MeasureBranch(
						availableSize,
						element,
						0,
						measuredElements
						);

					size.Width += branchSize.Width;
					size.Height = Math.Max(size.Height, branchSize.Height);
				}
			}

			return size;
		}
		protected virtual void MeasureDepth(
			UIElement headElement,
			int headDepth,
			Stack<DiagramItem> ancestors
			)
		{
			if (headElement == null) throw new ArgumentNullException("headElement");
			if (ancestors == null) throw new ArgumentNullException("ancestors");

			int currentDepth = GetDepth(headElement);
			if (currentDepth >= headDepth) return;

			SetDepth(headElement, headDepth);

			DiagramItem diagramItem = headElement as DiagramItem;
			if (
				(diagramItem != null)
				&& (diagramItem.Connectors != null)
				)
			{
				ancestors.Push(diagramItem);

				foreach (var connector in diagramItem.Connectors)
				{
					UIElement targetElement = this.GetTargetElement(connector.TargetItem);
					if (
						(targetElement != null)
						&& (!ancestors.Contains(targetElement))
						)
					{
						this.MeasureDepth(targetElement, headDepth + 1, ancestors);
					}
				}

				ancestors.Pop();
			}
		}

		protected virtual Size MeasureBranch(
			Size availableSize,
			UIElement headElement,
			int headDepth,
			List<UIElement> measuredElements
			)
		{
			headElement.Measure(availableSize);
			Size headSize = headElement.DesiredSize;
			measuredElements.Add(headElement);

			Size branchSpacing = new Size(12, 12);

			Size branchSize;

			DiagramItem diagramItem = headElement as DiagramItem;
			if (
				(diagramItem != null)
				&& (diagramItem.Connectors != null)
				)
			{
				double subbranchWidth = 0;
				double subbranchHeight = 0;
				int childDepth = headDepth + 1;

				foreach (var connector in diagramItem.Connectors)
				{
					if (connector.TargetItem != null)
					{
						if (connector.TargetItem == null) continue;

						UIElement targetElement = this.GetTargetElement(connector.TargetItem);
						if (targetElement == null) continue;

						if (measuredElements.Contains(targetElement)) continue;

						if (GetDepth(targetElement) == childDepth)
						{
							Size subbranchSize = this.MeasureBranch(
								availableSize,
								targetElement,
								childDepth,
								measuredElements
								);

							subbranchHeight = Math.Max(subbranchHeight, subbranchSize.Height);
							subbranchWidth += subbranchSize.Width + branchSpacing.Width;
						}
					}
				}

				branchSize = (subbranchHeight == 0)
					? headSize
					: new Size(
						Math.Max(headSize.Width, subbranchWidth),
						headSize.Height + branchSpacing.Height + subbranchHeight
						)
					;

				diagramItem.SubbranchSize = new Size(subbranchWidth, subbranchHeight);
			}
			else
			{
				branchSize = headSize;
			}

			SetBranchSize(headElement, branchSize);
			return branchSize;
		}

		protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
		{
			Size size = new Size();
			//return base.MeasureOverride(availableSize);

			Size branchSpacing = new Size(12, 12);

			Point arrangePoint = new Point();
			List<FrameworkElement> arrangedElements = new List<FrameworkElement>(this.Children.Count);
			foreach (FrameworkElement element in this.Children)
			{
				if (
					(GetDepth(element) == 0)
					&& !arrangedElements.Contains(element))
				{
					Size branchSize = this.ArrangeBranch(
						arrangePoint,
						element,
						0,
						arrangedElements
						);

					arrangePoint.X += branchSize.Width + branchSpacing.Width;

					size.Width += branchSize.Width + branchSpacing.Width;
					size.Height = Math.Max(size.Height, branchSize.Height);
				}
			}

			this.RouteConnectors();
			this.InvalidateVisual();

			return arrangeSize;// size;// base.ArrangeOverride(arrangeSize);
		}
		protected virtual Size ArrangeBranch(
			Point arrangePoint,
			FrameworkElement rootElement,
			int headDepth,
			List<FrameworkElement> arrangedElements
			)
		{
			Size branchSize = GetBranchSize(rootElement);
			Size headSize = rootElement.DesiredSize;
			Rect headRect = new Rect(
				new Point(arrangePoint.X + (branchSize.Width - headSize.Width) / 2, arrangePoint.Y),
				headSize
				);
			rootElement.Arrange(headRect);

			arrangedElements.Add(rootElement);

			Size branchSpacing = new Size(12, 12);

			DiagramItem diagramItem = rootElement as DiagramItem;
			if (
				(diagramItem != null)
				&& (diagramItem.Connectors != null)
				)
			{
				arrangePoint.X += (branchSize.Width - diagramItem.SubbranchSize.Width) / 2;
				arrangePoint.Y += branchSpacing.Height + headSize.Height;

				double subbranchWidth = 0;
				double subbranchHeight = 0;
				int childDepth = headDepth + 1;

				foreach (var connector in diagramItem.Connectors)
				{
					if (connector.TargetItem != null)
					{
						if (connector.TargetItem == null) continue;

						FrameworkElement targetElement = this.GetTargetElement(connector.TargetItem);
						if (targetElement == null) continue;

						if (arrangedElements.Contains(targetElement)) continue;

						if (GetDepth(targetElement) == childDepth)
						{
							Size subbranchSize = this.ArrangeBranch(
								arrangePoint,
								targetElement,
								childDepth,
								arrangedElements
								);

							subbranchHeight = Math.Max(subbranchHeight, subbranchSize.Height);
							subbranchWidth += subbranchSize.Width + branchSpacing.Width;
							arrangePoint.X += subbranchSize.Width + branchSpacing.Width;
						}
					}
				}

				branchSize = (subbranchHeight == 0)
					? headSize
					: new Size(
						Math.Max(headSize.Width, subbranchWidth),
						headSize.Height + branchSpacing.Height + subbranchHeight
						)
					;
			}
			else
			{
				branchSize = headSize;
			}

			return branchSize;
		}

		internal static int GetDepth(DependencyObject obj)
		{
			return (int)obj.GetValue(DepthProperty);
		}

		internal static void SetDepth(DependencyObject obj, int value)
		{
			obj.SetValue(DepthProperty, value);
		}

		// Using a DependencyProperty as the backing store for Depth.  This enables animation, styling, binding, etc...
		internal static readonly DependencyProperty DepthProperty =
			DependencyProperty.RegisterAttached("Depth", typeof(int), typeof(DiagramPanel), new UIPropertyMetadata(-1));



		internal static Size GetBranchSize(DependencyObject obj)
		{
			return (Size)obj.GetValue(BranchSizeProperty);
		}

		internal static void SetBranchSize(DependencyObject obj, Size value)
		{
			obj.SetValue(BranchSizeProperty, value);
		}

		// Using a DependencyProperty as the backing store for BranchSize.  This enables animation, styling, binding, etc...
		internal static readonly DependencyProperty BranchSizeProperty =
			DependencyProperty.RegisterAttached("BranchSize", typeof(Size), typeof(DiagramPanel), new UIPropertyMetadata(new Size()));

		#endregion

		#region Rendering
		protected override void OnRender(System.Windows.Media.DrawingContext dc)
		{
			base.OnRender(dc);

			foreach (var child in this.Children)
			{
				DiagramItem diagramItem = child as DiagramItem;
				if (
					(diagramItem != null)
					&& (diagramItem.Connectors != null)
					)
				{
					Connector[] connectors = diagramItem.Connectors.ToArray();
					for (int i = 0; i < connectors.Length; i++)
					{
						foreach (var connector in diagramItem.Connectors)
						{
							if (connector.Stroke == null) continue;
							if (connector.geometry == null) continue;


							//FrameworkElement targetElement = this.GetTargetElement(connector.TargetItem);
							//if (targetElement == null) continue;

							//Geometry route = this.RouteConnector(connector, diagramItem, targetElement);
							dc.DrawGeometry(null, connector.Stroke, connector.geometry);
						}
					}
				}
			}
		}

		private void RouteConnectors()
		{
			List<Rect> rects = new List<Rect>(this.Children.Count);

			foreach (var child in this.Children)
			{
				FrameworkElement element = child as FrameworkElement;
				if (element == null) continue;

				Rect rect = LayoutInformation.GetLayoutSlot(element);
				rects.Add(rect);
			}

			foreach (var child in this.Children)
			{
				DiagramItem diagramItem = child as DiagramItem;
				if (
					(diagramItem != null)
					&& (diagramItem.Connectors != null)
					)
				{
					Connector[] connectors = diagramItem.Connectors.ToArray();
					for (int i = 0; i < connectors.Length; i++)
					{
						Connector connector = connectors[i];
						if (connector.Stroke == null) continue;
						FrameworkElement targetElement = this.GetTargetElement(connector.TargetItem);
						if (targetElement == null) continue;

						connector.geometry = this.RouteConnector(connector, diagramItem, targetElement, rects.ToArray());
					}
				}
			}
		}

		private Geometry RouteConnector(
			Connector connector,
			DiagramItem sourceElement,
			FrameworkElement targetElement,
			Rect[] rects
			)
		{
			Thickness margin = sourceElement.Margin;
			//Point startPoint = sourceElement.TranslatePoint(
			//    new Point(
			//        (sourceElement.RenderSize.Width - margin.Left - margin.Right) / 2 + margin.Left,
			//    //diagramItem.RenderSize.Height - margin.Bottom
			//        sourceElement.RenderSize.Height
			//        ),
			//    this
			//    );
			Point startPoint = sourceElement.TranslatePoint(
				new Point(
					(sourceElement.RenderSize.Width) / 2,
					sourceElement.RenderSize.Height
					),
				this
				);

			Thickness targetMargin = targetElement.Margin;
			//Point endPoint = targetElement.TranslatePoint(
			//    new Point(
			//        (targetElement.RenderSize.Width - targetMargin.Left - targetMargin.Right) / 2 + targetMargin.Left,
			//    //targetMargin.Top
			//        0
			//        ),
			//    this
			//    );
			Point endPoint = targetElement.TranslatePoint(
				new Point(
					(targetElement.RenderSize.Width) / 2,
					0
					),
				this
				);

			//double length;
			//Point[] routePoints = DetermineRoute(startPoint, endPoint, 0, rects, out length);

			//PathSegment[] segments = new PathSegment[routePoints.Length + 1];
			//for (int i = 0; i < routePoints.Length; i++)
			//{
			//    segments[i] = new LineSegment(routePoints[i], true);
			//}
			//segments[routePoints.Length] = new LineSegment(endPoint, true);

			PathSegment[] segments = new PathSegment[] {
			    new LineSegment(new Point(startPoint.X,startPoint.Y+margin.Top), true),
			    new LineSegment(new Point(endPoint.X,endPoint.Y-targetMargin.Bottom),true),
			    new LineSegment(new Point(endPoint.X,endPoint.Y),true)
			};
			PathFigure figure = new PathFigure(startPoint, segments, false);
			PathGeometry geometry = new PathGeometry(new PathFigure[] { figure });
			return geometry;
		}
		private static Point[] DetermineRoute(
			Point startPoint,
			Point endPoint,
			int depth,
			IList<Rect> rects,
			out double length
			)
		{
			foreach (var rect in rects)
			{
				Point altPoint1 = new Point(), altPoint2 = new Point();
				bool intersects = Intersects(
					startPoint,
					endPoint,
					rect,
					ref altPoint1,
					ref altPoint2
					);

				if (intersects)
				{
					double route1Length;
					Point[] route1 = DetermineRoute(startPoint, altPoint1, endPoint, depth + 1, rects, out route1Length);
					double route2Length;
					Point[] route2 = DetermineRoute(startPoint, altPoint2, endPoint, depth + 1, rects, out route2Length);

					if (route1Length < route2Length)
					{
						length = route1Length;
						return route1;
					}
					else
					{
						length = route2Length;
						return route2;
					}
				}
			}
			double dx = (endPoint.X - startPoint.X);
			double dy = (endPoint.Y - startPoint.Y);
			length = Math.Sqrt(dx * dx + dy * dy);
			return new Point[] { startPoint, endPoint };
		}
		private static Point[] DetermineRoute(
			Point startPoint,
			Point wayPoint,
			Point endPoint,
			int depth,
			IList<Rect> rects,
			out double length
			)
		{
			double preLength;
			Point[] preroute = DetermineRoute(startPoint, wayPoint, 0, rects, out preLength);
			double postLength;
			Point[] postroute = DetermineRoute(wayPoint, endPoint, 0, rects, out postLength);

			length = preLength + postLength;
			return preroute.Concat(postroute).ToArray();
		}
		private static bool Intersects(
			Point p1,
			Point p2,
			Rect rect,
			ref Point altPoint1,
			ref Point altPoint2
			)
		{
			double a = p2.Y - p1.Y;
			double b = p2.X - p1.X;
			double c = a * p1.X + b * p1.Y;


			Point[] points = new Point[] {
				rect.TopLeft,
				rect.TopRight,
				rect.BottomLeft,
				rect.BottomRight
			};

			double lineAngle = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
			double dx = p2.X - p1.X;
			double dy = p2.Y - p1.Y;
			double lineLength = Math.Sqrt(dx * dx + dy * dy);

			Point minPoint = p2, maxPoint = p2;
			double minAngle = lineAngle, maxAngle = lineAngle;
			for (int i = 0; i < points.Length; i++)
			{
				Point p = points[i];
				double t = Math.Atan2(p.Y - p1.Y, p.X - p1.X);
				while (t < 0) t += Math.PI;

				if (t < minAngle)
				{
					minAngle = t;
					minPoint = p;
				}
				if (t > maxAngle)
				{
					maxAngle = t;
					maxPoint = p;
				}
			}

			bool intersects =
				(lineAngle < maxAngle)
				&& (lineAngle > minAngle)
				;
			if (intersects)
			{
				altPoint1 = minPoint;
				altPoint2 = maxPoint;
			}

			return intersects;
		}
		#endregion
	}
}
